﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Xml.Serialization;

namespace Perceiveit.Xml
{
    /// <summary>
    /// The XEntity class serves as a base class for all complex types that 
    /// are to be mapped to an XElement in an XML file. All objects that wish to 
    /// access the functionality of the XEntity framework must inherit from this
    /// class or one of its subclasses.
    /// </summary>
    /// <remarks>A standard XEntity does not know it's own name, only the XRootEntity knows it's own name.
    /// It is up to the Factories of XProperties to specify the names of XEntities when they are added to parents or read from an underlying XML source.
    /// This allows for greater repurposing of code and classes</remarks>
    public class XEntity : IXmlSerializable, ICloneable, IDeepCloneable
    {
        #region const

        /// <summary>
        /// Defines the name for an element that will be used as temporary storage 
        /// until it is added to a root document, or initialized
        /// </summary>
        protected const string UnInitializedElementName = "un-named";

        /// <summary>
        /// Defines the namespace for an element that will be used as temporary storage
        /// until this entity is added to a document or initialized
        /// </summary>
        protected const string UnInitializedElementNamespace = "";

        #endregion

        #region ivars

        /// <summary>
        /// The XElement that this object gets and sets its underlying values to
        /// </summary>
        private XElement _element;

        /// <summary>
        /// A flag to identify if this XEntity has been initialized with
        /// </summary>
        private bool _initialized = false;

        /// <summary>
        /// An IXPropertyBag to store parsed references to underlying XObjects in the Xml data
        /// </summary>
        private IXPropertyBag _props;

        #endregion

        //
        // properties
        //

        #region protected internal XElement Element {get;}

        /// <summary>
        /// Gets the XElement associated with this XEntity.
        /// </summary>
        /// <remarks>If the entity has not been initialized with an existing element then a new 
        /// empty element will be created for storage of child values.</remarks>
        protected internal XElement Element
        {
            get 
            {
                if (_element == null)
                {
                    _element = this.CreateEmptyXElement();
                    this.ReleaseAllProperties();
                }
                return _element;
            }
        }

        #endregion

        #region protected IXPropertyBag PropertyBag {get;}

        /// <summary>
        /// Gets the property bag associated with this XEntity used for the temporary storage
        /// of parsed values and inner complex content.
        /// </summary>
        /// <remarks>The IXProperty bag is created on demand through the CreatePropertyBag method</remarks>
        protected IXPropertyBag PropertyBag
        {
            get
            {
                if (_props == null)
                    _props = this.CreatePropertyBag();
                return _props;
            }
        }

        #endregion

        #region protected internal bool IsInitialized {get;}

        /// <summary>
        /// Gets the flag that identifies if this XEntity has been 
        /// initialized
        /// </summary>
        /// <remarks>An XEntity is initialized with an XElement using 
        /// the Init() method. Until this point the XEntity will use its
        /// own XElement node for value storage.
        /// </remarks>
        protected internal bool IsInitialized
        {
            get { return _initialized; }
        }

        #endregion

        #region protected internal bool HasExistingNode {get;}

        /// <summary>
        /// Identifies if this XEntity has got an XElement that can be used to store values in.
        /// This can either be through the Init() method or from CreateEmptyElement()
        /// </summary>
        protected internal bool HasExistingNode
        {
            get { return null != _element; }
        }

        #endregion


        //
        // initialization and instantiation methods
        //

        #region public void Init(XElement element)

        /// <summary>
        /// Initializes an XEntity with underlying XML data. If this XEntity has an existing element and values these will 
        /// be lost/overridden with the values from the provided Xml
        /// </summary>
        /// <param name="element">The element this XEntity should use to parse and store values in</param>
        /// <remarks>This is the primary method to use to associate an XElement within a document with an XEntity.
        /// Once initialized all property values will come from the underlying xml and set to the Xml.
        /// Inheritors can use the OnInitComplete() method to provide their own intialization methods</remarks>
        public void Init(XElement element)
        {
            if (element == null)
                throw new ArgumentNullException("node");
            this._element = element;
            if (_props != null && _props.Count > 0)
            {
                this.ReleaseAllProperties();
            }
            _initialized = true;
            this.OnInitComplete();

        }

        #endregion

        #region protected internal virtual void ReleaseAllProperties()

        /// <summary>
        /// Releases any references to known or parsed values from the underlying Xml data.
        /// </summary>
        /// <remarks>Inheritors can add additional functionality, but should always call the base implementation.
        /// </remarks>
        protected internal virtual void ReleaseAllProperties()
        {
            _props = null;
        }

        #endregion

        #region protected virtual void OnInitComplete()

        /// <summary>
        /// Occurs after the initialization of this XEntity with a new XElement
        /// </summary>
        /// <remarks>Inheritors should override this method to provide their own initialization</remarks>
        protected virtual void OnInitComplete()
        {

        }

        #endregion

        #region protected virtual XElement CreateEmptyXElement()

        /// <summary>
        /// Creates a new XElement for this XEntity to stores it's data 
        /// under.
        /// </summary>
        /// <returns>A new non-null XElement</returns>
        /// <remarks>The default implementation calls GetEntityName() to get a name for the element and 
        /// returns a new empty XElement with this name</remarks>
        protected virtual XElement CreateEmptyXElement()
        {
            XName name = this.GetEntityName();
            XElement node = new XElement(name);
            return node;
        }

        #endregion

        #region protected virtual XName GetEntityName()

        /// <summary>
        /// Gets an XName for this XEntity. By default this is XEntity.UnInitializedElementName, 
        /// XEntity.UnInitializedElementNameSpace although inheritors gan override this.
        /// </summary>
        /// <returns>A XName instance</returns>
        protected virtual XName GetEntityName()
        {
            string name = UnInitializedElementName;
            string ns = UnInitializedElementNamespace;
            return XName.Get(name, ns);
        }

        #endregion

        #region protected virtual IXPropertyBag CreatePropertyBag()

        /// <summary>
        /// Creates a new IXPropertyBag to store XPropertyCells for value retrieval
        /// </summary>
        /// <returns>A new IXPropertyBag</returns>
        /// <remarks>The default implementation returns an XPropertyDictionaryBag which
        /// stores its values in a dictionary. It may be faster for subclasses with 
        /// fewer frequently accessed values (less than 7 properties) to create and
        /// return an instance of the XPropertyListBag, however care should be
        /// taken as inheriting instances will use the same bag for storage
        /// so may add more properties.</remarks>
        protected virtual IXPropertyBag CreatePropertyBag()
        {
            return new XPropertyDictionaryBag();
        }

        #endregion

        //
        // property accessor methods
        //

        #region internal bool TryGetCell(XProperty property, out XPropertyCell cell)

        /// <summary>
        /// Attempts to retrieve an already parsed value for the XProperty from
        /// this instances IXPropertyBag.
        /// </summary>
        /// <param name="property"></param>
        /// <param name="cell"></param>
        /// <returns></returns>
        internal bool TryGetCell(XProperty property, out XPropertyCell cell)
        {
            return this.PropertyBag.TryGetCell(property, out cell);
        }

        #endregion

        #region internal void SetCell(XProperty property, XPropertyCell cell)

        /// <summary>
        /// Sets the XPropertyCell associated with this instances XProperty to
        /// the specified cell
        /// </summary>
        /// <param name="property"></param>
        /// <param name="cell"></param>
        internal void SetCell(XProperty property, XPropertyCell cell)
        {
            this.PropertyBag.SetCell(property, cell);
        }

        #endregion

        #region internal void ClearCell(XProperty property)

        /// <summary>
        /// Removes any XPropertyCell associated with the XProperty from this instances 
        /// IXPropertyBag 
        /// </summary>
        /// <param name="property">The property  to remove any cell for</param>
        internal void ClearCell(XProperty property)
        {
            this.PropertyBag.ClearCell(property);
        }

        #endregion

        //
        // object overrides
        //

        #region public override string ToString()

        /// <summary>
        /// Overrides the default ToString implementation to return the XElement's ToString() 
        /// value, if it has an existing XElement
        /// </summary>
        /// <returns>This XEntity.Element's ToString value </returns>
        public override string ToString()
        {
            return (this.HasExistingNode) ? this.Element.ToString() : base.ToString();
        }

        #endregion

        //
        // Interface implementation
        //


        #region IXmlSerializable Members

        /// <summary>
        /// Explicit interface implmentation of the GetSchema() method - 
        /// calls the protected GetSchema() instance method.
        /// </summary>
        /// <returns></returns>
        System.Xml.Schema.XmlSchema IXmlSerializable.GetSchema()
        {
            return this.GetSchema();
        }

        /// <summary>
        /// Overrideable method to get the XmlSchema for this instance. 
        /// The default implementation returns null.
        /// </summary>
        /// <returns></returns>
        protected virtual System.Xml.Schema.XmlSchema GetSchema()
        {
            return null;
        }

        /// <summary>
        /// IXmlSerializable method to read the Xml associated with this XEntity
        /// </summary>
        /// <param name="reader">The reader for the xml to load</param>
        /// <remarks>This method will move the reader to the first element it finds - if nescessary
        /// and load this as its data. This will also reset any values that have been assigned to it.</remarks>
        public virtual void ReadXml(System.Xml.XmlReader reader)
        {
            while (reader.NodeType != System.Xml.XmlNodeType.Element)
            {
                reader.Read();
                if (!reader.EOF)
                    return;
            }

            //found an element so load the Xml.Linq.XElement and initialize with this.
            XElement ele = XElement.Load(reader, LoadOptions.PreserveWhitespace);
            this.Init(ele);
        }

        /// <summary>
        /// IXmlSerializable method to write the Xml associated with this XEntity to the writer
        /// </summary>
        /// <param name="writer">The writer to output the xml to</param>
        /// <remarks>If this instance does not have any data, then nothing will be written</remarks>
        public virtual void WriteXml(System.Xml.XmlWriter writer)
        {
            if (this.HasExistingNode)
            {
                this.Element.WriteTo(writer);
            }
        }

        #endregion

        #region ICloneable Members

        /// <summary>
        /// Creates a <b>shallow</b> clone of this XEntity
        /// </summary>
        /// <returns></returns>
        public virtual object Clone()
        {
            return this.Clone(false);
        }

        #endregion

        #region IDeepClonable 

        /// <summary>
        /// Creates a clone of this object. If the value of deep is true then 
        /// all contained values will also be copied, rather than references
        /// being copied to the new instance as in a shallow clone.
        /// </summary>
        /// <param name="deep">True to specify a complete new object graph</param>
        /// <returns>A new instance of this XEntity</returns>
        public virtual object Clone(bool deep)
        {
            XEntity entity = (XEntity)this.MemberwiseClone();

            //do we need to copy the values too?
            if (deep && this.HasExistingNode)
            {
                XElement ele = new XElement(this.Element);
                if (this.IsInitialized)
                    entity.Init(ele);
                else
                {
                    entity._element = ele;
                    entity._initialized = false;
                    entity.ReleaseAllProperties();
                }
            }
            else
            {
                //we don't have an existing node so there is nothing to 
                //do here - no values set.
            }
            return entity;
        }

        #endregion
    }
}
